"""
Higher-level constraints on mappings of translated user input.

Fields provide what might be considered rudimentary validation during
translation, but they are limited in scope to single values.  Because
validators work on entire dictionaries of values, they can implement much more
elaborate rules enforcing patterns on user input.

To avoid indeterminate cases, most validators enforce their constraints only
if all the keys they expect are present in the subject ``dict`` being
validated.
"""

from formosa import ValidationError

__all__ = ['ValidationError', 'Same', 'Different', 'AtLeast', 'AtMost',
           'OnlyIf', 'Ascending', 'Descending', 'EqualTo', 'Unique',
           'IfAnyThen']

class Same(object):
    """Validator to assert that a set of values are all equal to each
    other."""

    messages = {'not_equal': 'Values are not the same'}

    def __init__(self, keys, alt_messages={}):
        """Create a validator where all the values from the specified keys
        must be equal."""
        self.keys = keys
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if any of the specified values from
        ``subject`` are not equal.  Skip validation if any value from
        ``subject`` is missing or ``None``."""
        if any((not subject.has_key(i)) or (subject[i] is None)
               for i in self.keys):
            return
        elif any(subject[i] != subject[j] for i in self.keys for j in self.keys):
            raise ValidationError(self.messages['not_equal'], self.keys)


class Different(object):
    """Validator to assert that a set of values are all different from one
    another."""

    messages = {'not_different': 'Values must be different'}

    def __init__(self, keys, alt_messages={}):
        """Create a validator where all the values from the specified keys
        must be different."""
        self.keys = keys
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if any of the specified values from
        ``subject`` are equal.  Skip validation if any value from ``subject``
        is missing or ``None``."""
        if any((not subject.has_key(i)) or (subject[i] is None)
               for i in self.keys):
            return
        elif any(subject[i] == subject[j]
                 for i in self.keys for j in self.keys if i != j):
            raise ValidationError(self.messages['not_different'], self.keys)


class AtLeast(object):
    """Validator to assert that at least a certain number of values are not
    ``None``."""

    messages = {'too_few_sing': 'At least one value must be entered',
                'too_few_pl': 'At least %d values must be entered'}

    def __init__(self, min, keys, alt_messages={}):
        """Create a validator where at least ``min`` values from the specified
        subset of keys must not be ``None``."""
        self.min = min
        self.keys = keys
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if fewer than the specified minumum
        number of values in ``subject`` are not ``None``.  Skip validation if
        ``subject`` is missing one or more key."""
        try:
            num = len([i for i in self.keys if subject[i] is not None])
        except KeyError:
            return
        if num < self.min:
            if self.min == 1:
                raise ValidationError(self.messages['too_few_sing'], self.keys)
            else:
                raise ValidationError(self.messages['too_few_pl'] % self.min,
                                      self.keys)


class AtMost(object):
    """Validator to assert that no more than a certain number of values are
    not ``None``."""

    messages = {'too_many_sing': 'No more than one value may be entered',
                'too_many_pl': 'No more than %d values may be entered'}

    def __init__(self, max, keys, alt_messages={}):
        """Create a new validator where no more than ``max`` values from the
        specified subset of keys may be not ``None``."""
        self.max = max
        self.keys = keys
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if more than the specified maximum number
        of values in ``subject`` are not ``None``.  Skip validation if
        ``subject`` is missing one or more key."""
        try:
            num = len([i for i in self.keys if subject[i] is not None])
        except KeyError:
            return
        if num > self.max:
            if self.max == 1:
                raise ValidationError(self.messages['too_many_sing'],
                                      self.keys)
            else:
                raise ValidationError(self.messages['too_many_pl'] % self.max,
                                      self.keys)


class OnlyIf(object):
    """Validator for when a set of fields may have values only if every field
    in another set does.

    It is strongly recommended that an alternative message be provided to
    instances of this validator, as the default message is not (and cannot be)
    very meaningful."""

    messages = {'reqs_not_met': 'Value may not be be entered unless the dependencies are met'}

    def __init__(self, contingent_keys, required_keys, alt_messages={}):
        """Create a new validator such that all fields with keys in the
        iterable ``contingent_key`` must be ``None`` unless every field with
        keys in ``required_keys`` is not ``None``."""
        self.contingent_keys = contingent_keys
        self.required_keys = required_keys
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if one or more contingent field has a
        value without all required field also having values."""
        try:
            num_contingent = len([i for i in self.contingent_keys
                                  if subject[i] is not None])
            num_required = len([i for i in self.required_keys
                                if subject[i] is not None])
        except KeyError:
            return
        if num_contingent > 0 and num_required < len(self.required_keys):
            raise ValidationError(self.messages['reqs_not_met'],
                                  self.contingent_keys)


class Ascending(object):
    """Validator to assert that a series of values are given in ascending
    order."""

    messages = {'not_ascending': 'Values are not in ascending order'}

    def __init__(self, keys, strict=False, alt_messages={}):
        """Create a new validator to assert that the corresponding values of
        the iterable ``keys`` are in ascending order.  If ``strict`` is true,
        compare with ``>``, otherwise compare with ``>=``."""
        self.keys = keys
        self.strict = strict
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if a value in ``subject`` is out of
        order."""
        last = None
        for key in self.keys:
            try:
                current = subject[key]
            except KeyError:
                return
            if last is not None:
                if (self.strict and last >= current) \
                    or (not self.strict and last > current):
                    raise ValidationError(self.messages['not_ascending'],
                                          self.keys)
            last = current


class Descending(object):
    """Validator to assert that a series of values are given in descending
    order."""

    messages = {'not_descending': 'Values are not in descending order'}

    def __init__(self, keys, strict=False, alt_messages={}):
        """Create a new validator to assert that the corresponding values of
        the iterable ``keys`` are in descending order.  If ``strict`` is true,
        compare with ``<``, otherwise compare with ``<=``."""
        self.keys = keys
        self.strict = strict
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if a value in ``subject`` is out of
        order."""
        last = None
        for key in self.keys:
            try:
                current = subject[key]
            except KeyError:
                return
            if last is not None:
                if (self.strict and last <= current) \
                    or (not self.strict and last < current):
                    raise ValidationError(self.messages['not_descending'],
                                          self.keys)
            last = current


class EqualTo(object):
    """Validator to assert that a set of fields are equal to a specific value
    according to ``==``."""

    messages = {'not_equal': 'Invalid value'}

    def __init__(self, value, keys, alt_messages={}):
        """Create a validator where each value from the specified keys must be
        equal to ``value``."""
        self.value = value
        self.keys = keys
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if any of the specified values from
        ``subject`` is not equal to the reference value.  Skip validation if
        any value from ``subject`` is missing or ``None``."""
        if any((not subject.has_key(i)) or (subject[i] is None)
               for i in self.keys):
            return
        elif any(subject[i] != self.value for i in self.keys):
            raise ValidationError(self.messages['not_equal'], self.keys)


class Unique(object):
    """Validator to assert that a set of values are unique to a corresponding
    set of SQLAlchemy model properties."""

    messages = {'not_unique_sing': 'Value has already been taken',
                'not_unique_pl': 'The given values have already been taken'}

    def __init__(self, session, mapper_or_class, propkeys, inst=None,
                 alt_messages={}):
        """Create a new validator to check for the uniqueness of values
        corresponding to a subset of a model's properties.

        - ``session``: SQLAlchemy session to be queried

        - ``mapper_or_class``: Mapper or mapped class to be queried

        - ``propkeys``: Mapping of property names to keys in the subject.  The
          validator queries for instances of the mapped class that have these
          values.

        - ``inst``: If only one object is found when querying the session, and
          if it ``is`` ``inst``, pass validation."""
        self.session = session
        self.mapper_or_class = mapper_or_class
        self.propkeys = propkeys
        self.inst = inst
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if one or more objects is found with the
        values in ``subject`` as indicated by ``propkeys``, unless only one
        object is found and it is the same as the object specified by
        ``inst``.  Skip validation if any of the specified values from
        ``subject`` (i.e. those specified by the values in ``propkeys``) is
        either missing or ``None``."""
        if any((not subject.has_key(i)) or (subject[i] is None)
               for i in self.propkeys.itervalues()):
            return
        try:
            criteria = dict((prop, subject[key]) for (prop, key)
                            in self.propkeys.iteritems())
        except KeyError:
            return
        query = self.session.query(self.mapper_or_class).filter_by(**criteria)
        count = query.count()
        invalid = False
        if count == 1:
            if not self.inst or self.inst != query.one():
                invalid = True
        elif count > 1:
            invalid = True
        else:
            invalid = False
        if invalid:
            msg = 'not_unique_sing' if len(self.propkeys) == 1 \
                else 'not_unique_pl'
            raise ValidationError(self.messages[msg], self.propkeys.keys())


class IfAnyThen(object):
    """Validator to assert that if any field from one set has been specified,
    then each field from another set must have a value."""

    messages = {'required_sing': 'A value is required',
                'required_pl': 'Values are required'}

    def __init__(self, if_set, then_set, alt_messages={}):
        """Create a new validator such that each field from ``then_set`` is
        required if any field from ``if_set`` is given."""
        self.if_set = if_set
        self.then_set = then_set
        self.messages = dict(self.__class__.messages, **alt_messages)

    def validate(self, subject):
        """Raise ``ValidationError`` if one or more values from ``if_set`` is
        present but any value from ``then_set`` is missing."""
        def given(field):
            return (subject.has_key(field) and subject[field] is not None)
        if any(given(i) for i in self.if_set) \
                and any(not given(i) for i in self.then_set):
            msg = self.messages['require_sing' if len(self.then_set) == 1
                                else 'required_pl']
            raise ValidationError(msg, self.then_set)
